package com.xes.hardwritingrecognition;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.support.annotation.NonNull;
import android.util.DisplayMetrics;
import android.util.Log;

import com.xes.hardwritingrecognition.opencv.OpenCVNumberProcessor;
import com.xes.hardwritingrecognition.opencv.OpenCVPathProcessor;

import org.opencv.core.Mat;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

/**
 * Handwriting recognition main entrance.
 *
 * @author wl
 * @see #start()
 * @see #destroy()
 * @since 2018/07/04 19:23
 */
public class HWRecognizer {
    public static boolean DEBUG_MODE = false;
    private static final String PART_OF_FOUR = "10";
    private static final String PART_OF_FIVE = "11";
    private static final String PART_OF_FIVE_2 = "12";
    private static final int STROKE_WIDTH = 3; // target stroke size in px
    private static final int DOT_THRESHOLD_W = 21; // threshold size of dot for width
    private static final int DOT_THRESHOLD_H = 25; // threshold size of dot for height

    private MainExecutor mTfExecutor;
    private ImageProcessor<Mat> mOpenCVProcessor;
    private OpenCVNumberProcessor mNumberProcessor;
    private List<String> mResults;
    private List<Path> mPaths;
    private Paint mPaint;
    private OssManager mOssManager;
    private float density;

    public HWRecognizer(Context context, String modelFileAssetName, MainExecutor.ClassifierType type) {
        MainExecutor.classifierType = type;
        init(context, null, modelFileAssetName, true);
    }

    public HWRecognizer(Context context, InputStream modelFileInputStream, MainExecutor.ClassifierType type) {
        MainExecutor.classifierType = type;
        init(context, modelFileInputStream, null, false);
    }

    private void init(Context context, InputStream modelFileInputStream, String assetsName, boolean isAssets) {
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        this.density = displayMetrics.density;
        mTfExecutor = new MainExecutor(context, modelFileInputStream, assetsName, isAssets);
        mTfExecutor.enableLogging(DEBUG_MODE);
        mOpenCVProcessor = new OpenCVPathProcessor(context);
        mOpenCVProcessor.setDebugMode(DEBUG_MODE);
        mNumberProcessor = new OpenCVNumberProcessor(context);
        mNumberProcessor.setDebugMode(DEBUG_MODE);
        mOssManager = OssManager.getInstance(context);
        mPaths = new ArrayList<>();
        mResults = new ArrayList<>();
        // init paint
        mPaint = new Paint();
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeWidth(10);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeJoin(Paint.Join.ROUND);
    }

    /**
     * Add one path when a stroke has been written
     *
     * @param path path
     */
    public String addPath(@NonNull Path path) {
        Path wrap = new Path(path);

        Bitmap bitmap = pathToBitmap(path, STROKE_WIDTH, 3, 20, false);
        if (bitmap != null) {
            mPaths.add(wrap);

            String result = recognize(bitmap);
            mResults.add(result);
            bitmap.recycle();
            return result;
        } else {
            mPaths.add(wrap);
            mResults.add(".");
            return ".";
        }
    }

    // recognize the bitmap and return the most similar label
    private String recognize(Bitmap bitmap) {
        // pre-work with openCV
        Mat mat = mOpenCVProcessor.process(bitmap);

        float[] data = new float[mat.cols() * mat.rows()];
        mat.get(0, 0, data);
        mat.release();
        // get result by using tensor-flow
        List<Classifier.Recognition> results = mTfExecutor.getResult(data);
        if (results.size() > 0) {
            // select the first result
            return results.get(0).getTitle();
        }

        return "";
    }

    /**
     * Start recognizing the input paths into String result.
     *
     * @return string
     * @see #addPath(Path) addPath
     */
    public String start() {
        if (mPaths.size() != mResults.size())
            return "error";

        Path wholePath = new Path();
        // analyze result
        StringBuilder sb = new StringBuilder();
        boolean skipNextAnalyze = false;
        for (int i = 0; i < mPaths.size(); i++) {
            if (!skipNextAnalyze) {
                skipNextAnalyze = analyze(i, sb);
            } else {
                skipNextAnalyze = false;
            }

            wholePath.addPath(mPaths.get(i));
        }
        String result = sb.toString();
        sendResultToOssIfNeeded(wholePath, result);
        return result;
    }

    // analyze and correct the results.
    // return true to skip the next analyze
    private boolean analyze(int i, StringBuilder sb) {
        boolean skipNext = false;
        Path path = mPaths.get(i);
        String res = mResults.get(i);
        String preRes = "", nextRes = "";
        if (i > 0)
            preRes = mResults.get(i - 1);
        if (i < mResults.size() - 1)
            nextRes = mResults.get(i + 1);
        switch (res) {
            case PART_OF_FOUR:
            case "2":
                // if current is part of '4' or '2':
                // 1. the next is '1' and is intersect with current; current must be '4', and skip next.
                // 2. the previous is '1' and is intersect with current; delete previous '1' and set current be '4'
                // 3. neither next nor previous is '1'; make current be '2'
                if (nextRes.equals("1") && isPathIntersect(path, mPaths.get(i + 1))) {
                    sb.append("4");
                    skipNext = true;
                    mResults.set(i + 1, ""); // the next is done, make next empty.
                } else if (preRes.equals("1") && isPathIntersect(path, mPaths.get(i - 1))) {
                    sb.deleteCharAt(sb.length() - 1);
                    sb.append("4");
                } else {
                    sb.append("2");
                }
                break;
            case PART_OF_FIVE:
                // if both are part of '5';
                // the right part of '5' is similar to '1', so we consider it.
                if (nextRes.equals(PART_OF_FIVE_2) && isPathNear5(path, mPaths.get(i + 1))) {
                    sb.append("5");
                    skipNext = true;
                    mResults.set(i + 1, "");
                } else if (preRes.equals(PART_OF_FIVE_2) && isPathNear5(path, mPaths.get(i - 1))) {
                    sb.deleteCharAt(sb.length() - 1);
                    sb.append("5");
                } else if (nextRes.equals(".") && isPathNear5(path, mPaths.get(i + 1))) {
                    sb.append("5");
                    skipNext = true;
                    mResults.set(i + 1, "");
                } else if (preRes.equals(".") && isPathNear5(path, mPaths.get(i - 1))) {
                    sb.deleteCharAt(sb.length() - 1);
                    sb.append("5");
                } else if (nextRes.equals("1") && isPathNear5(path, mPaths.get(i + 1))) {
                    sb.append("5");
                    skipNext = true;
                    mResults.set(i + 1, "");
                } else if (preRes.equals("1") && isPathNear5(path, mPaths.get(i - 1))) {
                    sb.deleteCharAt(sb.length() - 1);
                    sb.append("5");
                } else {
                    sb.append("3");
                }
                break;
            case PART_OF_FIVE_2:
                // we prior consider both parts are '5';
                // the left part of '5' is similar to '6', so we consider it.
                if (nextRes.equals(PART_OF_FIVE) && isPathNear5(mPaths.get(i + 1), path)) {
                    sb.append("5");
                    skipNext = true;
                    mResults.set(i + 1, "");
                } else if (nextRes.equals("6") && isPathNear5(mPaths.get(i + 1), path)) {
                    sb.append("5");
                    skipNext = true;
                    mResults.set(i + 1, "");
                } else if (preRes.equals("6") && isPathNear5(mPaths.get(i - 1), path)) {
                    sb.deleteCharAt(sb.length() - 1);
                    sb.append("5");
                } else {
                    sb.append("1");
                }
                break;
            default:
                sb.append(res);
                break;
        }

        return skipNext;
    }

    // validity for '4'
    private boolean isPathIntersect(Path path1, Path path2) {
        Path tmp = new Path();
        tmp.op(path1, path2, Path.Op.INTERSECT);
        RectF rect = new RectF();
        tmp.computeBounds(rect, true);

        return rect.width() != 0f && rect.height() != 0f;
    }

    // validity for '5'
    private boolean isPathNear5(Path main, Path sub) {
        RectF mainRect = new RectF();
        main.computeBounds(mainRect, true);
        RectF subRect = new RectF();
        sub.computeBounds(subRect, true);

        return subRect.centerX() > mainRect.left && subRect.centerY() < mainRect.centerY();
    }

    /**
     * Clear all input paths
     */
    public void clearPaths() {
        mPaths.clear();
        mResults.clear();
    }

    // convert path to a proper bitmap
    // if the path is too small, return null
    // scaleType: 0 - not scale, 1 - width, 2 - height, 3 - bigger one of width & height
    // destSize: dest size to scale to
    private Bitmap pathToBitmap(Path path, int strokeWidth, int scaleType, double destSize, boolean needBg) {
        RectF rectF = new RectF();
        path.computeBounds(rectF, true);
        // if path is too small, ignore it
        float width = rectF.width();
        float height = rectF.height();
        if (width <= DOT_THRESHOLD_W * density && height <= DOT_THRESHOLD_H * density)
            return null;
        // pre-scale the path, to reduce bitmap memory
        float min = Math.min(width, height);
        if (min > 128) {
            float prescale = min / 128;
            width /= prescale;
            height /= prescale;
            Matrix matrix = new Matrix();
            matrix.setScale(1 / prescale, 1 / prescale);
            path.transform(matrix);
            path.computeBounds(rectF, true);
        }
        // strokeWidth multiply a proper factor, to ensure correctness after scaling
        double factor = 1;
        if (scaleType == 3) {
            scaleType = height >= width ? 2 : 1;
        }
        if (scaleType == 1) {
            factor = width / destSize;
        } else if (scaleType == 2) {
            factor = height / destSize;
        }

        strokeWidth *= factor;
        Bitmap bitmap = Bitmap.createBitmap((int) width + strokeWidth,
                (int) height + strokeWidth,
                Bitmap.Config.RGB_565);
        Canvas canvas = new Canvas(bitmap);
        canvas.translate(-rectF.left + strokeWidth / 2, -rectF.top + strokeWidth / 2);
        mPaint.setStrokeWidth(strokeWidth);
        canvas.drawPath(path, mPaint);

        Log.i("hwr", "size: " + bitmap.getByteCount());

        return bitmap;
    }

    // do scale and upload to oss
    private void sendResultToOssIfNeeded(Path path, String result) {
        if (!mOssManager.isEnabled())
            return;
        Bitmap origin = pathToBitmap(path, STROKE_WIDTH, 2, 36d, true);
        if (origin == null)
            return;

        Matrix matrix = new Matrix();
        int height = origin.getHeight();
        if (height > 36f) {
            float scale = 36f / height;
            matrix.setScale(scale, scale);
        }
        Bitmap newBmp = Bitmap.createBitmap(origin, 0, 0, origin.getWidth(), height, matrix, false);
        if (!origin.isRecycled())
            origin.recycle();

        mOssManager.uploadAnImage(newBmp, result);
    }

    /**
     * Start recognizing the whole input bitmap to String
     * @param bitmap bitmap
     * @return string
     * @deprecated use {@link #addPath(Path)} and {@link #start()} instead.
     */
    public String start(@NonNull Bitmap bitmap) {
        // pre-work with openCV
        Vector<Mat> mats = mNumberProcessor.process(bitmap);
        //
        StringBuilder sb = new StringBuilder();
        for (Mat mat : mats) {
            float[] data = new float[mat.cols() * mat.rows()];
            mat.get(0, 0, data);
            // get result by using tensor-flow
            List<Classifier.Recognition> results = mTfExecutor.getResult(data);
            if (results.size() > 0) {
                // select the first result
                sb.append(results.get(0).getTitle());
            }
        }

        return sb.toString();
    }

    /**
     * Destroy HWRecognizer.
     */
    public void destroy() {
        mTfExecutor.closeClassifier();
    }
}
